<?php

namespace App\Core;

use App\Exception\BindingResolutionException;
use function Couchbase\defaultDecoder;

class Ioc
{

    protected $bindings = [];

    protected $instances = [];

    protected static $instance;

    public function __construct()
    {
        $this->setInstance($this);
        $this->instance('ioc', $this);
    }

    /**
     * @return $this
     */
    public static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new static();
        }
        return self::$instance;
    }

    /**
     * @param mixed $instance
     */
    public function setInstance($instance): void
    {
        self::$instance = $instance;
    }

    public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropBinding($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }
        if (!$concrete instanceof \Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }
        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    public function single($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }


    public function dropBinding($abstract)
    {
        unset($this->bindings[$abstract], $this->instances[$abstract]);
    }

    public function instance($abstract, $instance)
    {
        $this->dropBinding($abstract);
        $this->instances[$abstract] = $instance;
        return $instance;
    }

    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete, $parameters);
            }
            return $container->make($concrete, $parameters);
        };
    }

    public function make($abstract, array $parameters = [])
    {
        if (empty($parameters) && isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }
        if (!isset($this->bindings[$abstract])) {
            $this->bindings[$abstract] = ['concrete' => $abstract, 'shared' => false];
        }
        $binding = $this->bindings[$abstract];

        $object = $this->build($binding['concrete'], $parameters);

        if ($binding['shared']) {
            $this->instances[$abstract] = $object;
        }
        return $object;
    }

    protected function build($concrete, $parameters = [])
    {
        if ($concrete instanceof \Closure) {
            return $concrete($this, $parameters);
        }
        if (is_object($concrete)) {
            return $concrete;
        }
        $reflectClass = new \ReflectionClass($concrete);
        if (!$reflectClass->isInstantiable()) {

            throw new BindingResolutionException("Target [$concrete] is not instantiable.");
        }
        $constructor = $reflectClass->getConstructor();
        if (is_null($constructor)) {
            return new $concrete;
        }
        $dependencies = $constructor->getParameters();

        $instance = $this->resolveDependencies($dependencies);
        return $reflectClass->newInstanceArgs($instance);
    }

    protected function resolveDependencies(array $dependencies)
    {
        $result = [];
        foreach ($dependencies as $dependency) {
            $result[] = $this->make($dependency->getClass()->name);
        }
        return $result;
    }
}